/**
 * Copyright (c) 2021-2022 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "plugins/ecmascript/runtime/base/builtins_base.h"
#include "plugins/ecmascript/runtime/ecma_runtime_call_info.h"
#include "plugins/ecmascript/runtime/ecma_string.h"
#include "plugins/ecmascript/runtime/ecma_vm.h"
#include "plugins/ecmascript/runtime/global_env.h"
#include "plugins/ecmascript/runtime/js_array.h"
#include "plugins/ecmascript/runtime/js_handle.h"
#include "plugins/ecmascript/runtime/js_hclass.h"
#include "plugins/ecmascript/runtime/js_map.h"
#include "plugins/ecmascript/runtime/js_map_iterator.h"
#include "plugins/ecmascript/runtime/js_object-inl.h"
#include "plugins/ecmascript/runtime/js_tagged_value.h"
#include "plugins/ecmascript/runtime/js_thread.h"
#include "plugins/ecmascript/runtime/object_factory.h"
#include "plugins/ecmascript/runtime/tagged_array-inl.h"
#include "plugins/ecmascript/tests/runtime/common/test_helper.h"
#include "utils/bit_utils.h"

// NOLINTNEXTLINE(google-build-using-namespace)
using namespace ark::ecmascript;
// NOLINTNEXTLINE(google-build-using-namespace)
using namespace ark::ecmascript::builtins;

// NOLINTBEGIN(readability-magic-numbers,modernize-avoid-c-arrays)

namespace ark::test {
using JSMap = ecmascript::JSMap;

class BuiltinsMapTest : public testing::Test {
public:
    static void SetUpTestCase()
    {
        GTEST_LOG_(INFO) << "SetUpTestCase";
    }

    static void TearDownTestCase()
    {
        GTEST_LOG_(INFO) << "TearDownCase";
    }

    void SetUp() override
    {
        TestHelper::CreateEcmaVMWithScope(instance_, thread_, scope_);
    }

    void TearDown() override
    {
        TestHelper::DestroyEcmaVMWithScope(instance_, scope_);
    }

    class TestClass {
    public:
        static JSTaggedValue TestFunc(EcmaRuntimeCallInfo *argv)
        {
            int num = builtins_common::GetCallArg(argv, 0)->GetInt();
            JSArray *jsArray = JSArray::Cast(builtins_common::GetThis(argv)->GetTaggedObject());
            int length = jsArray->GetArrayLength() + num;
            jsArray->SetArrayLength(argv->GetThread(), length);
            return JSTaggedValue::Undefined();
        }
    };

protected:
    // NOLINTNEXTLINE(misc-non-private-member-variables-in-classes)
    JSThread *thread_ {nullptr};

private:
    PandaVM *instance_ {nullptr};
    EcmaHandleScope *scope_ {nullptr};
};

JSMap *CreateBuiltinsMap(JSThread *thread)
{
    JSHandle<GlobalEnv> env = thread->GetEcmaVM()->GetGlobalEnv();
    JSHandle<JSFunction> newTarget(env->GetMapFunction());
    // 4 : test case
    auto ecmaRuntimeCallInfo = TestHelper::CreateEcmaRuntimeCallInfo(thread, JSTaggedValue(*newTarget), 4);
    ecmaRuntimeCallInfo->SetFunction(newTarget.GetTaggedValue());
    ecmaRuntimeCallInfo->SetThis(JSTaggedValue::Undefined());

    [[maybe_unused]] auto prev = TestHelper::SetupFrame(thread, ecmaRuntimeCallInfo.get());
    JSTaggedValue result = map::MapConstructor(ecmaRuntimeCallInfo.get());

    EXPECT_TRUE(result.IsECMAObject());
    JSMap *jsMap = JSMap::Cast(reinterpret_cast<TaggedObject *>(result.GetRawData()));
    return jsMap;
}
// new Map("abrupt").toString()
TEST_F(BuiltinsMapTest, CreateAndGetSize)
{
    ObjectFactory *factory = thread_->GetEcmaVM()->GetFactory();
    JSHandle<GlobalEnv> env = thread_->GetEcmaVM()->GetGlobalEnv();
    JSHandle<JSFunction> newTarget(env->GetMapFunction());
    JSHandle<JSMap> map(thread_, CreateBuiltinsMap(thread_));

    auto ecmaRuntimeCallInfo = TestHelper::CreateEcmaRuntimeCallInfo(thread_, JSTaggedValue::Undefined(), 6);
    ecmaRuntimeCallInfo->SetFunction(JSTaggedValue::Undefined());
    ecmaRuntimeCallInfo->SetThis(map.GetTaggedValue());
    ecmaRuntimeCallInfo->SetCallArg(0, JSTaggedValue::Undefined());

    {
        [[maybe_unused]] auto prev = TestHelper::SetupFrame(thread_, ecmaRuntimeCallInfo.get());
        JSTaggedValue result = map::proto::GetSize(ecmaRuntimeCallInfo.get());

        EXPECT_EQ(result.GetRawData(), JSTaggedValue(0).GetRawData());
    }
    JSHandle<TaggedArray> array(factory->NewTaggedArray(5));
    for (int i = 0; i < 5; i++) {
        JSHandle<TaggedArray> internalArray(factory->NewTaggedArray(2));
        internalArray->Set(thread_, 0, JSTaggedValue(i));
        internalArray->Set(thread_, 1, JSTaggedValue(i));
        auto arr = JSArray::CreateArrayFromList(thread_, internalArray);
        array->Set(thread_, i, arr);
    }
    JSHandle<JSArray> values = JSArray::CreateArrayFromList(thread_, array);
    auto ecmaRuntimeCallInfo1 = TestHelper::CreateEcmaRuntimeCallInfo(thread_, JSTaggedValue::Undefined(), 6);
    ecmaRuntimeCallInfo1->SetFunction(newTarget.GetTaggedValue());
    ecmaRuntimeCallInfo1->SetThis(map.GetTaggedValue());
    ecmaRuntimeCallInfo1->SetCallArg(0, values.GetTaggedValue());
    ecmaRuntimeCallInfo1->SetNewTarget(newTarget.GetTaggedValue());
    {
        [[maybe_unused]] auto prev = TestHelper::SetupFrame(thread_, ecmaRuntimeCallInfo1.get());
        JSTaggedValue result1 = map::MapConstructor(ecmaRuntimeCallInfo1.get());

        EXPECT_EQ(JSMap::Cast(reinterpret_cast<TaggedObject *>(result1.GetRawData()))->GetSize(), 5);
    }
}

TEST_F(BuiltinsMapTest, SetAndHas)
{
    ObjectFactory *factory = thread_->GetEcmaVM()->GetFactory();
    // create jsMap
    JSHandle<JSMap> map(thread_, CreateBuiltinsMap(thread_));
    JSHandle<JSTaggedValue> key(factory->NewFromCanBeCompressString("key"));

    auto ecmaRuntimeCallInfo = TestHelper::CreateEcmaRuntimeCallInfo(thread_, JSTaggedValue::Undefined(), 8);
    ecmaRuntimeCallInfo->SetFunction(JSTaggedValue::Undefined());
    ecmaRuntimeCallInfo->SetThis(map.GetTaggedValue());
    ecmaRuntimeCallInfo->SetCallArg(0, key.GetTaggedValue());
    ecmaRuntimeCallInfo->SetCallArg(1, JSTaggedValue(static_cast<int32_t>(1)));

    JSMap *jsMap;
    {
        [[maybe_unused]] auto prev = TestHelper::SetupFrame(thread_, ecmaRuntimeCallInfo.get());
        JSTaggedValue result1 = map::proto::Has(ecmaRuntimeCallInfo.get());

        EXPECT_EQ(result1.GetRawData(), JSTaggedValue::False().GetRawData());

        // test Set()
        JSTaggedValue result2 = map::proto::Set(ecmaRuntimeCallInfo.get());

        EXPECT_TRUE(result2.IsECMAObject());
        jsMap = JSMap::Cast(reinterpret_cast<TaggedObject *>(result2.GetRawData()));
        EXPECT_EQ(jsMap->GetSize(), 1);
    }

    // test Has()
    auto ecmaRuntimeCallInfo1 = TestHelper::CreateEcmaRuntimeCallInfo(thread_, JSTaggedValue::Undefined(), 8);
    ecmaRuntimeCallInfo1->SetFunction(JSTaggedValue::Undefined());
    ecmaRuntimeCallInfo1->SetThis(JSTaggedValue(jsMap));
    ecmaRuntimeCallInfo1->SetCallArg(0, key.GetTaggedValue());
    ecmaRuntimeCallInfo1->SetCallArg(1, JSTaggedValue(static_cast<int32_t>(1)));
    {
        [[maybe_unused]] auto prev = TestHelper::SetupFrame(thread_, ecmaRuntimeCallInfo1.get());
        JSTaggedValue result3 = map::proto::Has(ecmaRuntimeCallInfo1.get());

        EXPECT_EQ(result3.GetRawData(), JSTaggedValue::True().GetRawData());
    }
}

TEST_F(BuiltinsMapTest, ForEach)
{
    // generate a map has 5 entries{key1:0,key2:1,key3:2,key4:3,key5:4}
    ObjectFactory *factory = thread_->GetEcmaVM()->GetFactory();
    JSHandle<GlobalEnv> env = thread_->GetEcmaVM()->GetGlobalEnv();
    JSHandle<JSMap> map(thread_, CreateBuiltinsMap(thread_));
    char keyArray[] = "key0";
    for (int i = 0; i < 5; i++) {
        keyArray[3] = '1' + i;
        JSHandle<JSTaggedValue> key(factory->NewFromCanBeCompressString(keyArray));
        auto ecmaRuntimeCallInfo = TestHelper::CreateEcmaRuntimeCallInfo(thread_, JSTaggedValue::Undefined(), 8);
        ecmaRuntimeCallInfo->SetFunction(JSTaggedValue::Undefined());
        ecmaRuntimeCallInfo->SetThis(map.GetTaggedValue());
        ecmaRuntimeCallInfo->SetCallArg(0, key.GetTaggedValue());
        ecmaRuntimeCallInfo->SetCallArg(1, JSTaggedValue(static_cast<int32_t>(i)));

        [[maybe_unused]] auto prev = TestHelper::SetupFrame(thread_, ecmaRuntimeCallInfo.get());
        JSTaggedValue result1 = map::proto::Set(ecmaRuntimeCallInfo.get());
        EXPECT_TRUE(result1.IsECMAObject());
        JSMap *jsMap = JSMap::Cast(reinterpret_cast<TaggedObject *>(result1.GetRawData()));
        EXPECT_EQ(jsMap->GetSize(), i + 1);
    }
    // test foreach;
    JSHandle<JSArray> jsArray(JSArray::ArrayCreate(thread_, JSTaggedNumber(0)));
    JSHandle<JSFunction> func = factory->NewJSFunction(env, reinterpret_cast<void *>(TestClass::TestFunc));

    auto ecmaRuntimeCallInfo1 = TestHelper::CreateEcmaRuntimeCallInfo(thread_, JSTaggedValue::Undefined(), 8);
    ecmaRuntimeCallInfo1->SetFunction(JSTaggedValue::Undefined());
    ecmaRuntimeCallInfo1->SetThis(map.GetTaggedValue());
    ecmaRuntimeCallInfo1->SetCallArg(0, func.GetTaggedValue());
    ecmaRuntimeCallInfo1->SetCallArg(1, jsArray.GetTaggedValue());

    [[maybe_unused]] auto prev = TestHelper::SetupFrame(thread_, ecmaRuntimeCallInfo1.get());
    JSTaggedValue result2 = map::proto::ForEach(ecmaRuntimeCallInfo1.get());

    EXPECT_EQ(result2.GetRawData(), JSTaggedValue::VALUE_UNDEFINED);
    EXPECT_EQ(jsArray->GetArrayLength(), 10);
}

TEST_F(BuiltinsMapTest, DeleteAndRemove)
{
    ObjectFactory *factory = thread_->GetEcmaVM()->GetFactory();
    // create jsMap
    JSHandle<JSMap> map(thread_, CreateBuiltinsMap(thread_));

    // add 40 keys
    char keyArray[] = "key0";
    for (int i = 0; i < 40; i++) {
        keyArray[3] = '1' + i;
        JSHandle<JSTaggedValue> key(thread_, factory->NewFromCanBeCompressString(keyArray).GetTaggedValue());
        auto ecmaRuntimeCallInfo = TestHelper::CreateEcmaRuntimeCallInfo(thread_, JSTaggedValue::Undefined(), 8);
        ecmaRuntimeCallInfo->SetFunction(JSTaggedValue::Undefined());
        ecmaRuntimeCallInfo->SetThis(map.GetTaggedValue());
        ecmaRuntimeCallInfo->SetCallArg(0, key.GetTaggedValue());
        ecmaRuntimeCallInfo->SetCallArg(1, JSTaggedValue(static_cast<int32_t>(i)));

        [[maybe_unused]] auto prev = TestHelper::SetupFrame(thread_, ecmaRuntimeCallInfo.get());
        JSTaggedValue result1 = map::proto::Set(ecmaRuntimeCallInfo.get());

        EXPECT_TRUE(result1.IsECMAObject());
        JSMap *jsMap = JSMap::Cast(reinterpret_cast<TaggedObject *>(result1.GetRawData()));
        EXPECT_EQ(jsMap->GetSize(), i + 1);
    }
    // whether jsMap has delete key
    keyArray[3] = '1' + 8;
    JSHandle<JSTaggedValue> deleteKey(factory->NewFromCanBeCompressString(keyArray));

    auto ecmaRuntimeCallInfo1 = TestHelper::CreateEcmaRuntimeCallInfo(thread_, JSTaggedValue::Undefined(), 6);
    ecmaRuntimeCallInfo1->SetFunction(JSTaggedValue::Undefined());
    ecmaRuntimeCallInfo1->SetThis(map.GetTaggedValue());
    ecmaRuntimeCallInfo1->SetCallArg(0, deleteKey.GetTaggedValue());

    [[maybe_unused]] auto prev = TestHelper::SetupFrame(thread_, ecmaRuntimeCallInfo1.get());
    JSTaggedValue result2 = map::proto::Has(ecmaRuntimeCallInfo1.get());

    EXPECT_EQ(result2.GetRawData(), JSTaggedValue::True().GetRawData());

    // delete
    JSTaggedValue result3 = map::proto::Delete(ecmaRuntimeCallInfo1.get());

    EXPECT_EQ(result3.GetRawData(), JSTaggedValue::True().GetRawData());

    // check deleteKey is deleted
    JSTaggedValue result4 = map::proto::Has(ecmaRuntimeCallInfo1.get());

    EXPECT_EQ(result4.GetRawData(), JSTaggedValue::False().GetRawData());
    JSTaggedValue result5 = map::proto::GetSize(ecmaRuntimeCallInfo1.get());

    EXPECT_EQ(result5.GetRawData(), JSTaggedValue(39).GetRawData());

    // clear
    JSTaggedValue result6 = map::proto::Clear(ecmaRuntimeCallInfo1.get());
    EXPECT_EQ(result6.GetRawData(), JSTaggedValue::VALUE_UNDEFINED);
    EXPECT_EQ(map->GetSize(), 0);
}

TEST_F(BuiltinsMapTest, Species)
{
    ObjectFactory *factory = thread_->GetEcmaVM()->GetFactory();
    JSHandle<GlobalEnv> env = thread_->GetEcmaVM()->GetGlobalEnv();
    JSHandle<JSTaggedValue> map(thread_, CreateBuiltinsMap(thread_));

    // test species
    JSHandle<JSTaggedValue> speciesSymbol = env->GetSpeciesSymbol();
    EXPECT_TRUE(!speciesSymbol.GetTaggedValue().IsUndefined());

    JSHandle<JSFunction> newTarget(env->GetMapFunction());

    JSTaggedValue value =
        JSObject::GetProperty(thread_, JSHandle<JSTaggedValue>(newTarget), speciesSymbol).GetValue().GetTaggedValue();
    JSHandle<JSTaggedValue> valueHandle(thread_, value);
    EXPECT_EQ(value, newTarget.GetTaggedValue());

    // to string tag
    JSHandle<JSTaggedValue> toStringTagSymbol = env->GetToStringTagSymbol();
    JSHandle<EcmaString> stringTag(JSObject::GetProperty(thread_, map, toStringTagSymbol).GetValue());
    JSHandle<EcmaString> str = factory->NewFromCanBeCompressString("Map");
    EXPECT_TRUE(!stringTag.GetTaggedValue().IsUndefined());
    EXPECT_TRUE(EcmaString::StringsAreEqual(*str, *stringTag));

    JSHandle<JSFunction> constructor = JSHandle<JSFunction>::Cast(JSTaggedValue::ToObject(thread_, valueHandle));
    EXPECT_EQ(JSHandle<JSObject>(map)->GetPrototype(thread_), constructor->GetFunctionPrototype());

    JSHandle<JSTaggedValue> key1(factory->NewFromCanBeCompressString("set"));
    JSTaggedValue value1 = JSObject::GetProperty(thread_, map, key1).GetValue().GetTaggedValue();
    EXPECT_FALSE(value1.IsUndefined());

    JSHandle<JSTaggedValue> key2(factory->NewFromCanBeCompressString("has"));
    JSTaggedValue value2 = JSObject::GetProperty(thread_, map, key1).GetValue().GetTaggedValue();
    EXPECT_FALSE(value2.IsUndefined());

    JSHandle<JSTaggedValue> key3(factory->NewFromCanBeCompressString("clear"));
    JSTaggedValue value3 = JSObject::GetProperty(thread_, map, key1).GetValue().GetTaggedValue();
    EXPECT_FALSE(value3.IsUndefined());

    JSHandle<JSTaggedValue> key4(factory->NewFromCanBeCompressString("size"));
    JSTaggedValue value4 = JSObject::GetProperty(thread_, map, key1).GetValue().GetTaggedValue();
    EXPECT_FALSE(value4.IsUndefined());

    JSHandle<JSTaggedValue> key5(factory->NewFromCanBeCompressString("delete"));
    JSTaggedValue value5 = JSObject::GetProperty(thread_, map, key1).GetValue().GetTaggedValue();
    EXPECT_FALSE(value5.IsUndefined());

    JSHandle<JSTaggedValue> key6(factory->NewFromCanBeCompressString("forEach"));
    JSTaggedValue value6 = JSObject::GetProperty(thread_, map, key1).GetValue().GetTaggedValue();
    EXPECT_FALSE(value6.IsUndefined());

    JSHandle<JSTaggedValue> key7(factory->NewFromCanBeCompressString("get"));
    JSTaggedValue value7 = JSObject::GetProperty(thread_, map, key1).GetValue().GetTaggedValue();
    EXPECT_FALSE(value7.IsUndefined());
}

TEST_F(BuiltinsMapTest, GetIterator)
{
    JSHandle<JSTaggedValue> map(thread_, CreateBuiltinsMap(thread_));
    auto ecmaRuntimeCallInfo = TestHelper::CreateEcmaRuntimeCallInfo(thread_, JSTaggedValue::Undefined(), 4);
    ecmaRuntimeCallInfo->SetFunction(JSTaggedValue::Undefined());
    ecmaRuntimeCallInfo->SetThis(map.GetTaggedValue());
    [[maybe_unused]] auto prev = TestHelper::SetupFrame(thread_, ecmaRuntimeCallInfo.get());

    // test Values()
    JSTaggedValue result = map::proto::Values(ecmaRuntimeCallInfo.get());
    JSHandle<JSMapIterator> iter(thread_, result);
    EXPECT_TRUE(iter->IsJSMapIterator());
    EXPECT_EQ(IterationKind::VALUE, IterationKind(iter->GetIterationKind().GetInt()));
    EXPECT_EQ(JSMap::Cast(map.GetTaggedValue().GetTaggedObject())->GetLinkedMap(), iter->GetIteratedMap());

    // test Keys()
    JSTaggedValue result1 = map::proto::Keys(ecmaRuntimeCallInfo.get());
    JSHandle<JSMapIterator> iter1(thread_, result1);
    EXPECT_TRUE(iter1->IsJSMapIterator());
    EXPECT_EQ(IterationKind::KEY, IterationKind(iter1->GetIterationKind().GetInt()));

    // test entries()
    JSTaggedValue result2 = map::proto::Entries(ecmaRuntimeCallInfo.get());
    JSHandle<JSMapIterator> iter2(thread_, result2);
    EXPECT_TRUE(iter2->IsJSMapIterator());
    EXPECT_EQ(IterationKind::KEY_AND_VALUE, IterationKind(iter2->GetIterationKind().GetInt()));
}
}  // namespace ark::test

// NOLINTEND(readability-magic-numbers,modernize-avoid-c-arrays)
